module dlua.wrapper;

import dlua.all;
import std.variant;

static import str = std.string;

public enum LuaType : int
{
    None = -1,
    Nil = 0,
    Boolean = 1,
    LightUserData = 2,
    Number = 3,
    String = 4,
    Table = 5,
    Function = 6,
    UserData = 7,
    Thread = 8
}

alias extern(C) int function(lua_State*) lua_DFunction;


public class LuaVariable
{
private:

    Variant _variantData;
    lua_CFunction _functionPointer;
    LuaType _dataType = LuaType.None;
    bool _isCFunction = false;

public:

    this()
    {
    }

    this(lua_DFunction func)
    {
        Data = func;
    }

    this(lua_CFunction func)
    {
        Data = func;
    }

    this(char[] val)
    {
        this.Data = val.idup;
    }

    this(string val)
    {
        this.Data = val;
    }

    this(int val)
    {
        this.Data = val;
    }

    this(double val)
    {
        this.Data = val;
    }

    this(bool val)
    {
        this.Data = val;
    }

    this(LuaVariable[LuaVariable] val)
    {
        this.Data = val;
    }

    this(LuaVariable[string] val)
    {
        this.Data = val;
    }

    ~this()
    {
        if (this.DataType == LuaType.Table)
        {
            if (_variantData.convertsTo!(LuaVariable[LuaVariable]))
            {
                foreach (LuaVariable chKey, LuaVariable chValue; this.TableValue)
                {
                    delete(chKey);
                    delete(chValue);
                }
            }
            else if (_variantData.convertsTo!(LuaVariable[string]))
            {
                foreach (string chKey, LuaVariable chValue; this.GlobalTableValue)
                {
                    delete(chValue);
                }
            }
        }
    }

    LuaType DataType()
    {
        return _dataType;
    }

    void DataType(LuaType lt)
    {
        _dataType = lt;
    }

    void Data(bool data)
    {
        _variantData = data;
        _dataType = LuaType.Boolean;
    }

    void Data(int data)
    {
        _variantData = data;
        _dataType = LuaType.Number;
    }

    void Data(double data)
    {
        _variantData = data;
        _dataType = LuaType.Number;
    }

    void Data(string data)
    {
        _variantData = data;
        _dataType = LuaType.String;
    }

    void Data(char[] data)
    {
        _variantData = data;
        _dataType = LuaType.String;
    }

    void Data(lua_DFunction functionptr)
    {
        _functionPointer = cast(lua_CFunction)functionptr;
        _dataType = LuaType.Function;
    }

    void Data(lua_CFunction functionptr)
    {
        _functionPointer = functionptr;
        _dataType = LuaType.Function;
    }

    void Data(LuaVariable[LuaVariable] data)
    {
        _variantData = data;
        _dataType = LuaType.Table;
    }

    void Data(LuaVariable[string] data)
    {
        _variantData = data;
        _dataType = LuaType.Table;
    }

    int IntegerValue()
    {
        if (_variantData.convertsTo!(int))
        {
            return _variantData.get!(int);
        }
        return 0;
    }

	bool IsInitialized()
	{
		return _variantData.hasValue;
	}

    string StringValue()
    {
        return _variantData.toString();
    }

    double DoubleValue()
    {
        if (_variantData.convertsTo!(double))
        {
            return _variantData.get!(double);
        }
        return 0;
    }


    bool BooleanValue()
    {
        if (_variantData.convertsTo!(bool))
        {
            return _variantData.get!(bool);
        }
        return 0;
    }

    lua_CFunction FunctionValue()
    {
        if (_functionPointer !is null)
        {
            return _functionPointer;
        }
        return null;
    }

    LuaVariable[LuaVariable] TableValue()
    {
        if (_variantData.convertsTo!(LuaVariable[LuaVariable]))
        {
            return *_variantData.peek!(LuaVariable[LuaVariable]);
        }
        return null;
    }

    LuaVariable[string] GlobalTableValue()
    {
        if (_variantData.convertsTo!(LuaVariable[string]))
        {
            return *_variantData.peek!(LuaVariable[string]);
        }
        return null;
    }

    bool IsCFunction()
    {
        return _isCFunction;
    }

    void IsCFunction(bool iscfunc)
    {
        _isCFunction = iscfunc;
    }

    string Description()
    {
        char[] desc = ("[" ~ str.toString(cast(int)this.DataType) ~ "] ").dup;
        if (this.DataType == LuaType.Function)
        {
            desc ~= (this.IsCFunction ? "<C Extern>" : "<Native Lua>").dup;
        }
        else if (this.DataType == LuaType.Table)
        {
            if (_variantData.convertsTo!(LuaVariable[LuaVariable]))
            {
                desc ~= ("<" ~ str.toString(this.TableValue.length) ~ " items.>").dup;
            }
            if (_variantData.convertsTo!(LuaVariable[string]))
            {
                desc ~= ("<" ~ str.toString(this.GlobalTableValue.length) ~ " items.>").dup;
            }
        }
        else
        {
            desc ~= this.StringValue.dup;
        }

        return desc.idup;
    }

    int opCmp(LuaVariable v)
    {
        return this._variantData.opCmp(v._variantData);
    }

    bool opEquals(LuaVariable v)
    {
        return this._variantData == v._variantData;
    }

    // possibly add operator overloading if needed
}

public class Lua
{
private:
    static Lua _instance;
    private lua_State* lua;
    private LuaVariable[] args;
    private LuaVariable[string]  globals;

    this(lua_State* L)
    {
        lua = L;
    }

    LuaVariable getLuaVariable(int index)
    {
        LuaVariable v = new LuaVariable();

        if (index == LUA_GLOBALSINDEX)
            v.DataType = LuaType.Table;
        else
            v.DataType = cast(LuaType)lua_type(lua, index);

        switch (v.DataType)
        {
            case LuaType.String:
            {
                v.Data = str.toString(lua_tostring(lua, index));
                break;
            }
            case LuaType.Number:
            {
                v.Data = lua_tonumber(lua, index);
                break;
            }
            case LuaType.Boolean:
            {
                v.Data = lua_toboolean(lua, index);
                break;
            }
            case LuaType.Function:
            {
                v.IsCFunction = (lua_iscfunction(lua, index) != 0);
                v.Data = lua_tocfunction(lua, index);
                break;
            }
            case LuaType.Table:
            {
                LuaVariable[LuaVariable] tableData;
                LuaVariable[string] globalData;

                lua_pushnil(lua);
                while (lua_next(lua, index) != 0)
                {
                    LuaType keyType = cast(LuaType)lua_type(lua, -2);
                    LuaType valType = cast(LuaType)lua_type(lua, -1);

                    LuaVariable vKey = null,
                        vValue = null;
                    //LuaVariable vKey = getLuaVariable(-2);
                    //LuaVariable vValue = getLuaVariable(-1);

                    if (keyType != LuaType.Table)
                    {
                        vKey = getLuaVariable(-2);

                        // this really applies to global variables only
                        if ((keyType == LuaType.String) && (valType == LuaType.Table) &&
                            (vKey.StringValue == "_G" || vKey.StringValue == "package"))
                        {
                            // will have to skip this sucka because it causes everything to blow up
                            lua_pop(lua, 1);
                            continue;
                        }
                    }
                    else
                    {
                        lua_pushvalue(lua, -2);
                        vValue = getLuaVariable(lua_gettop(lua));
                        lua_pop(lua, 1);
                    }

                    if (valType != LuaType.Table)
                    {
                        vValue = getLuaVariable(-1);
                    }
                    else
                    {
                        lua_pushvalue(lua, -1);
                        vValue = getLuaVariable(lua_gettop(lua));
                        lua_pop(lua, 1);
                    }

                    if (index == LUA_GLOBALSINDEX)
                        globalData[vKey.StringValue] = vValue;
                    else
                        tableData[vKey] = vValue;

                    lua_pop(lua, 1);
                }

                if (index == LUA_GLOBALSINDEX)
                    v.Data = globalData;
                else
                    v.Data = tableData;

                break;
            }
            case LuaType.Nil:
            default:
            {
                break;
            }
        }

        return v;
    }

public:
    static Lua Instance(lua_State* L)
    {
        if (_instance is null) _instance = new Lua(L);
        else if (L == _instance.lua) _instance.lua = L;
        _instance.args.length = 0;
        return _instance;
    }

    static Lua Instance()
    {
        if (_instance is null)
        {
            lua_State* L = luaL_newstate();
            luaL_openlibs(L);
            _instance = new Lua(L);
        }
        else
        {
            _instance.args.length = 0;
        }

        return _instance;
    }

    ~this()
    {
        lua_close(lua);
        delete(args);
        foreach (string chKey, LuaVariable chValue; globals)
        {
            delete(chKey);
            delete(chValue);
        }
    }

    void Register(string funcName, lua_DFunction func)
    {
        lua_register(lua, funcName.dup.ptr, cast(int(*)(void** L))func);

        if (globals.length > 0) globals[funcName] = new LuaVariable(func);
    }

    void Register(string varName, string value)
    {
        lua_pushstring(lua, value.dup.ptr);
        lua_setglobal(lua, varName.dup.ptr);

        if (globals.length > 0) globals[varName] = new LuaVariable(value);
    }

    void Register(string varName, int value)
    {
        lua_pushinteger(lua, value);
        lua_setglobal(lua, varName.dup.ptr);

        if (globals.length > 0) globals[varName] = new LuaVariable(value);
    }

    void Register(string varName, double value)
    {
        lua_pushnumber(lua, value);
        lua_setglobal(lua, varName.dup.ptr);

        if (globals.length > 0) globals[varName] = new LuaVariable(value);
    }

    void Register(string varName, bool value)
    {
        lua_pushboolean(lua, value);
        lua_setglobal(lua, varName.dup.ptr);

        if (globals.length > 0) globals[varName] = new LuaVariable(value);
    }

    void Register(string varName, LuaVariable[LuaVariable] hash)
    {
        lua_newtable(lua);

        PushTableVariables(hash);
        lua_setglobal(lua, varName.dup.ptr);

        if (globals.length > 0) globals[varName] = new LuaVariable(hash);
    }
	
	void Return(LuaVariable var)
	{
		if (var.DataType == LuaType.Table)
		{
			lua_newtable(lua); 
			PushTableVariables(var.TableValue);
		}
		else
		{
			PushVariable(var);
		}
	}

    void PushVariable(LuaVariable var)
    {
        switch (var.DataType)
        {
            case LuaType.Number:
            {
                lua_pushnumber(lua, var.DoubleValue);
                break;
            }
            case LuaType.String:
            {
                lua_pushstring(lua, var.StringValue.dup.ptr);
                break;
            }
            case LuaType.Boolean:
            {
                lua_pushboolean(lua, var.BooleanValue);
                break;
            }
            case LuaType.Function:
            {
                lua_pushcfunction(lua, var.FunctionValue); // cast(int(*)(void** L))
                break;
            }
            //case LuaType.Table:
            //case LuaType.LightUserData:
            //case LuaType.UserData:
            case LuaType.Nil:
            case LuaType.None:
            default:
            {
                lua_pushnil(lua);
                break;
            }
        }
    }

    void PushTableVariables(LuaVariable[LuaVariable] hash)
    {
        foreach (LuaVariable chKey, LuaVariable chValue; hash)
        {
            if (chValue.DataType == LuaType.Table)
            {
                CreateTable(chKey, chValue.TableValue);
            }
            else
            {
                PushVariable(chKey);
                PushVariable(chValue);
                lua_settable(lua, -3);
            }
        }
    }

    void CreateTable(LuaVariable key, LuaVariable[LuaVariable] hash)
    {
        lua_newtable(lua);
        PushVariable(key);
        lua_pushvalue(lua, -2);
        lua_settable(lua, -4);

        PushTableVariables(hash);

        lua_pop(lua, 1);
    }


    int Execute(string code)
    {
        return luaL_dostring(lua, code.dup.ptr);
    }

    LuaVariable[string] Globals()
    {
        if (globals.length == 0)
        {
            LuaVariable v = getLuaVariable(LUA_GLOBALSINDEX);
            globals = v.GlobalTableValue;
        }
        return globals;
    }

    LuaVariable[] Arguments()
    {
        if (args.length == 0)
        {
            auto n = lua_gettop(lua);
            int depth = 0;
            for (auto i=0; i < n; i++)
            {
                args ~= getLuaVariable(i+1);
            }
        }

        return args;
    }
}
